Serverless FrameworkのStep Functions用プラグインでAWSサービスプロキシを設定してみた
はじめに
こんにちは、中山です。
先日積読になっていたAWS Computeのブログを読むという徳の高い行為をしていました。該当のブログは以下です。
Step FunctionsのアクティビティとAPI Gatewayを組み合わせて手動の承認処理を導入するアーキテクチャについて紹介したものになります。大分前の話になりますが、API GatewayのAWSサービスプロキシにStep Functionsが対応した発表を受けて、具体的なユースケースを紹介してくれたようです。
私は文章を読むだけだとあまり理解が進まないので、新しいことを勉強する際には実際に自分で作ってみるようにしてます。後述しますが、アーキテクチャがサーバレスな形になっていたのでServerless Frameworkで作ってみました。内部的にStep Functionsを使っているので、以前ブログで紹介したhorike37/serverless-step-functionsを使ったのですが、機能がかなりアップデートされていたのでそちらを中心にご紹介したいと思います。
検証環境
- Node.js: 8.2.1
- Serverless Framework: 1.19.0
- serverless-step-functions: 1.1.0
アーキテクチャ
今回作成するアーキテクチャを説明します。先程のブログから構成図を引用します。
まとめると以下のようなフローになります。
- API GatewayのAWSサービスプロキシでStep Functionsを呼び出せるようにしておく
- CloudWatch Eventsで定期的にLambdaをInvoke
- InvokeされたLambdaがStep Functionsのタスクの状態を監視
- タスクが存在した場合はInputで渡された情報を元にSESでメール送信
- メール本文に記載されたAPI Gatewayのエンドポイントに応じて後続のタスクへ遷移するか選択
この仕組みを利用することで、ステートマシーンの中に手動の承認処理をサーバレスな形で導入することが可能です。全て自動化されたフローにするのはまずいような処理をStep Functionsで行いたい場合に便利なのではないでしょうか。
やってみた
今回作成したリポジトリは以下です。ご自由にお使いください。こちらの主要なコードを元に解説します。
- knakayama/serverless-manual-approval
-
serverless.yml
service: serverless-manual-approval frameworkVersion: ">=1.19.0 <2.0.0" custom: config: ${file(config.yml)} provider: name: aws runtime: nodejs6.10 stage: ${self:custom.config.stage} region: ap-northeast-1 memorySize: 128 timeout: 60 iamRoleStatements: - Effect: Allow Action: states:GetActivityTask Resource: "arn:aws:states:#{AWS::Region}:#{AWS::AccountId}:activity:${self:custom.config.stepFunctions.activityName}" - Effect: Allow Action: ses:SendEmail Resource: "*" plugins: - serverless-pseudo-parameters - serverless-step-functions package: individually: true exclude: - "**" functions: func: name: ManualStepActivityWorker handler: src/handlers/func/index.handler environment: ACTIVITY_ARN: "arn:aws:states:#{AWS::Region}:#{AWS::AccountId}:activity:${self:custom.config.stepFunctions.activityName}" SERVICE_ENDPOINT: Fn::Join: [ "", [ "https://", Ref: "ApiGatewayRestApi", ".execute-api.", Ref: "AWS::Region", ".amazonaws.com/${self:custom.config.stage}" ] ] package: include: - src/handlers/func/*.js events: - schedule: rate(1 minute) stepFunctions: stateMachines: promotionApproval: events: - http: path: fail method: GET - http: path: succeed method: GET definition: Comment: "Employee promotion process!" StartAt: ManualApproval States: ManualApproval: Type: Task Resource: "arn:aws:states:#{AWS::Region}:#{AWS::AccountId}:activity:${self:custom.config.stepFunctions.activityName}" TimeoutSeconds: 3600 End: true activities: - ${self:custom.config.stepFunctions.activityName} resources: ${file(resource.yml)}
46 - 66行目でStep Functionsのステートマシーンとアクティビティを定義しています。さらにこちらがすごいのですが、 events
を指定することでAPI GatewayのREST APIリソースなどを作成し、Step FunctionsをAWSサービスプロキシとして設定可能です。Serverless FrameworkはLambdaとひも付ける形でAPI Gatewayを定義することは容易ですが、今回のようにAWSサービスプロキシの設定をする場合は自分でテンプレートを作成しなければならず、少々面倒でした。それが一般的な serverless.yml
と同じ形式で設定できるのはかなり便利ですね。今回はタスクを承認するために成功/失敗用のメソッドを作成しました。
resource.yml
--- AWSTemplateFormatVersion: "2010-09-09" Description: Serverless Manual Approval Stack Parameters: LogGroupRetentionInDays: Type: Number Default: ${self:custom.config.logGroup.retentionInDays} Resources: FuncLogGroup: Type: AWS::Logs::LogGroup Properties: RetentionInDays: Ref: LogGroupRetentionInDays ApiGatewayRestApi: Type: AWS::ApiGateway::RestApi Properties: Name: StepFunctionsAPI ApiGatewayMethodFailGet: Type: AWS::ApiGateway::Method Properties: Integration: Uri: Fn::Join: [ "", [ "arn:aws:apigateway:", Ref: "AWS::Region", ":states:action/SendTaskFailure" ] ] PassthroughBehavior: WHEN_NO_TEMPLATES RequestTemplates: application/json: | { "cause": "Reject link was clicked.", "error": "Rejected", "taskToken": "$input.params('taskToken')" } RequestParameters: method.request.querystring.taskToken: false ApiGatewayMethodSucceedGet: Type: AWS::ApiGateway::Method Properties: Integration: Uri: Fn::Join: [ "", [ "arn:aws:apigateway:", Ref: "AWS::Region", ":states:action/SendTaskSuccess" ] ] PassthroughBehavior: WHEN_NO_TEMPLATES RequestTemplates: application/json: | { "output": "\"Approve link was clicked.\"", "taskToken": "$input.params('taskToken')" } RequestParameters: method.request.querystring.taskToken: false ApigatewayToStepFunctionsRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/AWSStepFunctionsFullAccess
主に horike37/serverless-step-functions
で作成したAPI Gatewayの設定をしています。プラグインのREADMEに記載されているようにある程度 serverless.yml
側でAPI Gatewayの設定が可能ですが、執筆時点(2017/08/04)ではすべてのパラメータに対応しているわけではないようです。そのため、Serverless Frameworkのオーバーライドを利用して、プラグインで定義されたリソースを上書きしています。オーバーライドを軽く説明すると、Serverless Frameworkで作成されるCloudFormationのリソースと同じ名前でリソースを定義することにより、パラメータをカスタマイズできる機能です。今回の場合であれば以下のリソース名になるようなので、同じ名前で設定してあげれば上書きが可能です。
ApiGatewayMethodFailGet
- SendTaskFailure APIを送るためのメソッド
ApiGatewayMethodSucceedGet
- SendTaskSuccessを送るためのメソッド
ApigatewayToStepFunctionsRole
- API GatewayからStep Functionsを操作する際に利用するIAM Role
API Gatewayのメソッドについて少し触れると、 Integration - RequestTemplates
と RequestParameters
パラメータでtaskTokenをパラメータ形式で処理できるように定義させています。
src/handlers/func/index.js
const AWS = require('aws-sdk'); class Worker { constructor(event, context, callback) { this.event = event; this.context = context; this.callback = callback; this.stepfunctions = new AWS.StepFunctions(); this.ses = new AWS.SES({ region: 'us-east-1' }); } getActivityTask() { const params = { activityArn: process.env.ACTIVITY_ARN, }; return this.stepfunctions.getActivityTask(params).promise(); } sendEmail(data) { const input = JSON.parse(data.input); const params = { Destination: { ToAddresses: [ input.managerEmailAddress, ], }, Message: { Subject: { Data: 'Your Approval Needed for Promotion!', Charset: 'UTF-8', }, Body: { Html: { Data: 'Hi!<br />' + `${input.employeeName} has been nominated for promotion!<br />` + 'Can you please approve:<br />' + `${process.env.SERVICE_ENDPOINT}/succeed?taskToken=${encodeURIComponent(data.taskToken)}<br />` + 'Or reject:<br />' + `${process.env.SERVICE_ENDPOINT}/fail?taskToken=${encodeURIComponent(data.taskToken)}`, Charset: 'UTF-8', }, }, }, Source: input.managerEmailAddress, ReplyToAddresses: [ input.managerEmailAddress, ], }; return this.ses.sendEmail(params).promise(); } start() { this.getActivityTask() .then((data) => { console.log(data); return this.sendEmail(data); }) .then((data) => { console.log(data); this.callback(null, data); }) .catch((err) => { console.log(err); this.callback(err); }); } } module.exports.handler = (event, context, callback) => { new Worker(event, context, callback).start(); };
AWSのブログに記載されているコードを元に作成しました。やってることは単純です。Step FunctionsのGetActivityTask APIを呼び出してタスクの状態を監視、データを取得できた場合はSESのSendEmail APIでメールを送信しているだけです。
32 - 39行目でメール本文に記載するAPI Gatewayのエンドポイントへのリンクを作成しています。GetActivityTask APIのレスポンスで返されたtaskTokenを含めることにより、API Gatewayからタスクに対してAPIを発行できるようにしています。
9行目でSESのクライアントを設定していますが、東京リージョンにまだ来ていないので北部バージニアリージョンを利用しました。現時点でCloudFormationがSESに未対応ということもあり、SES周りは手動で環境を作成しています。サンドボックス内の場合は送信するメールアドレスに対して事前に承認する必要がある点はご注意ください。
動作確認
いつものようにServerless Frameworkをデプロイしたら動作確認してみます。まずリポジトリのトップディレクトリにある input.json
を以下のように修正してください。
input.json
{ "managerEmailAddress": "<SESからメール送信可能なメールアドレス>", "employeeName" : "<適当な名前>" }
続いてステートマシーンを実行します。以下のコマンドで実行可能です。
$ yarn invoke:stepf
しばらくするとメールアドレスに以下のようなメールが届くと思います。
承認/否認用リンクのどちらかをクリックするとステートマシーンの実行が完了します。
まとめ
いかがだったでしょうか。
horike37/serverless-step-functions
を中心にステートマシーンへ手動の承認処理を導入する方法をご紹介しました。一般的にサーバレスアーキテクチャはEC2等のインスタンスを管理せずに済むという点が強調されがちですが、アーキテクチャ全体をコードという形で管理できるという点も魅力の1つです。その際、Serverless Frameworkやそのプラグインを利用することで簡単にコードを定義できるのは便利ですね。引き続きサーバレス分野の動向を追っていきたいと思います。
本エントリがみなさんの参考になれば幸いに思います。